import math
import informationMap

def schemaFromJsonValue(value):
    result = InfoSchema()
    for setName in value:
        setValue = value[setName]
        descr = descrFromJsonValue(setValue)
        result.addDescriptionSet(setName, descr)
    return result

class InfoSchema:
    def __init__(self):
        self.sets = {}
    def addDescriptionSet(self, name, value=None):
        if value is None:
            value = DescriptionSet()
        self.sets[name]=value
    def addAttribute(self, setname, attname, value):
        setValue = self.sets[setname]
        setValue.addAttribute(attname, value)
    def hasDescriptionSet(self, name):
        return name in self.sets
    def flatten(self):
        "eliminate multivalued attributes by moving or adding relationship sets"
        result = InfoSchema()
        # create empty existing sets matching self in result
        for setName in self.sets:
            #result[setName] = DescriptionSet()
            result.addDescriptionSet(setName)
        # flatten sets, possibly adding new relationship sets
        for setname in self.sets:
            setValue = self.sets[setname]
            setValue.flatten(setname, result)
        return result
    def jsonValue(self):
        D = {}
        for setname in self.sets:
            setvalue = self.sets[setname]
            D[setname] = setvalue.jsonValue()
        return D
    def dumpToCanvas(self, c, cfg):
        D = Dependancies(self.sets)
        D.assignAllPositions()
        L = informationMap.LayoutGrid()
        connections = []
        for setName in self.sets:
            setObject = self.sets[setName]
            setObject.dumpToCanvas(setName, self.sets, L, c, cfg, connections)
        for connection in connections:
            #pr "CONNECTING", connection
            L.connect(connection["name"], connection["attname"], connection["attType"],
                      connection["isMandatory"], connection["isOnto"], connection["isOneToOne"], connection["isMultiple"])
        L.draw(c,cfg)

class Dependancies:
    def __init__(self, sets):
        self.sets = sets
        self.fromMap = {}
        self.toMap = {}
        self.setNameToCoordinates = {}
        self.coordinatesToSetName = {}
        self.minI = self.minJ = 0
        self.candidatePositions = {(0,0):1}
        for setName in sets:
            setObject = sets[setName]
            for typeName in setObject.types():
                self.addDependancy(setName, typeName)
    def addDependancy(self, fromname, toname):
        sets = self.sets
        if fromname in sets and toname in sets:
            addmap(self.fromMap, fromname, toname)
            addmap(self.toMap, toname, fromname)
        else:
            # ignore external dependancies
            pass
    def assignAllPositions(self):
        assert not self.setNameToCoordinates and not self.coordinatesToSetName
        unassigned = self.sets.copy()
        #assert unassigned, "nothing to assign"
        currentPositions = []
        while unassigned:
            chosenName = chosenCost = chosenPosition = None
            currentNames = unassigned.keys()
            currentNames.sort()
            #currentPositions = self.candidatePositions.keys()
            ps = self.candidatePositions.keys()
            ps.sort()
            for p in ps:
                if not p in currentPositions:
                    currentPositions.append(p)
            #currentPositions.sort()
            # find the unassigned with the highest valence
            maxvalence = -1
            for name in currentNames:
                froms = self.fromMap.get(name, {})
                tos = self.toMap.get(name, {})
                assigneds = 0
                allfromsAssigned = True
                for f in froms:
                    if not f in unassigned:
                        assigneds += 1.2 #2
                    else:
                        allfromsAssigned = False
                alltosAssigned = True
                for t in tos:
                    if not t in unassigned:
                        assigneds += 1
                    else:
                        alltosAssigned = False
                bonus = 0
                if tos and allfromsAssigned:
                    bonus += 50
                if froms and alltosAssigned:
                    bonus += 30
                valence = 5*len(froms)+4*len(tos)+10*assigneds + bonus
                if valence>maxvalence:
                    chosenName = name
                    maxvalence = valence
            for position in currentPositions:
                cost = self.positionCost(position, chosenName)
                if chosenCost is None or chosenCost>cost:
                    chosenCost = cost
                    chosenPosition = position
            assert chosenName is not None and chosenPosition is not None
            currentPositions.remove(chosenPosition)
            del unassigned[chosenName]
            self.assignPosition(chosenPosition, chosenName)
        # record the positions
        for setname in self.sets:
            setposition = self.setNameToCoordinates[setname]
            setobject = self.sets[setname]
            (i,j) = setposition
            setobject.positionAt(i-self.minI, j-self.minJ)
    def assignPosition(self, position, setName):
        #pr "assigning", setName, "position", position
        #pr "from candidates", self.candidatePositions
        self.setNameToCoordinates[setName] = position
        self.coordinatesToSetName[position] = setName
        (i,j) = position
        self.minI = min(self.minI, i)
        self.minJ = min(self.minJ, j)
        self.addNeighbors(position)
        del self.candidatePositions[position]
    def positionCost(self, position, setName):
        result = (self.adjacencyCosts(position, setName, self.fromMap, True) + 
                self.adjacencyCosts(position, setName, self.toMap, False))
        #pr "positionCost for", setName, "at", position, "totals", result
        return result
    def adjacencyCosts(self, position, setName, map, preferUpperLeft=True):
        result = (abs(position[0])+abs(position[1]))/2 # prefer positions near origin
        for fromName in map.get(setName, {}):
            if fromName in self.setNameToCoordinates:
                fromposition = self.setNameToCoordinates[fromName]
                distancePenalty = (rectangularDistance(fromposition, position, preferUpperLeft)-gravitationalAttraction(fromposition, position, preferUpperLeft))*2
                #distancePenalty = (rectangularDistance(fromposition, position, preferUpperLeft))*2
                result += distancePenalty # try to get close to neighbors...
            else:
                result -= 1 # alternatively, prefer to choose elts with more connections first
        #pr "for", setName, "at", position, "cost is", result
        return result
    def addNeighbors(self, position):
        (i,j) = position
        coords = self.coordinatesToSetName
        candidates = self.candidatePositions
        for i0 in range(i-1,i+2):
            for j0 in range(j-1,j+2):
                p0 = (i0, j0)
                if not p0 in coords:
                    candidates[p0] = 1

def gravitationalAttraction(pfrom, pto, preferUpperLeft=True):
    d = rectangularDistance(pfrom, pto, preferUpperLeft)
    if d<1:
        d = 1
    return 100/d

def rectangularDistance(pfrom, pto, preferUpperLeft=True):
    # consider making horizontal more expensive than vertical
    (ifrom,jfrom) = pfrom
    (ito,jto) = pto
    di = ifrom-ito
    dj = jfrom-jto
    #"""
    if preferUpperLeft:
        if di>=0:# or dj>=0:
            di -= 0.2
            #dj -=1
    else:
        if di<=0:# and dj<=0:
            di += 0.2
            #dj += 1
    #        """
    #return abs(di)+abs(dj)
    #return math.sqrt(di*di + dj*dj)
    return di*di + dj*dj

def addmap(map, a, b):
    D = map.get(a, {})
    D[b] = a
    map[a] = D

def descrFromJsonValue(val):
    result = DescriptionSet()
    for attrValue in val:
        attribute = attrFromJsonValue(attrValue)
        result.addAttribute(attribute.name, attribute)
    return result

class DescriptionSet:
    def __init__(self):
        self.attributes = {}
        self.attributeNames = [] # ordering
        self.position = None
    def positionAt(self, i, j):
        self.position = (i,j)
    def addAttribute(self, name, attr):
        assert name not in self.attributes, "ambiguous attribute "+repr(name)
        self.attributeNames.append(name)
        self.attributes[name] = attr
    def flatten(self, myname, schema):
        "move or expand any multivalued attribute"
        for attName in self.attributes:
            attr = self.attributes[attName]
            attr.flatten(self, myname, schema)
    def dumpToCanvas(self, name, sets, L, c, cfg, connections):
        R = informationMap.RelationBox(name, c, cfg)
        (i,j) = self.position
        #pr "adding box", name, "at", i,j
        L.addBox(R, i, j)
        for attname in self.attributeNames:
            attribute = self.attributes[attname]
            attType = attribute.typeName
            if attType in sets:
                # add link
                R.addAttribute(attname, attribute.isIdentifier)
                #L.connect(name, attname, attType)
                connections.append({"name":name, "attname":attname, "attType":attType,
                                    "isIdentifier": attribute.isIdentifier,
                                    "isMandatory": attribute.isMandatory,
                                    "isOnto": attribute.isOnto,
                                    "isOneToOne": attribute.isOneToOne,
                                    "isMultiple": attribute.isMultiple})
            else:
                # add simple attr
                attname += ":"+attType
                attnotation = None
                if not attribute.isMandatory:
                    attnotation=" ?"
                if attribute.isMultiple:
                    if attribute.isMandatory:
                        attnotation = " +"
                    else:
                        attnotation = " *"
                if attnotation:
                    attname += attnotation
                R.addAttribute(attname, attribute.isIdentifier)
    def types(self):
        for attname in self.attributes:
            att = self.attributes[attname]
            yield att.typeName
    def jsonValue(self):
        L = []
        for attname in self.attributeNames:
            att = self.attributes[attname]
            #D[attname] = att.jsonValue()
            L.append( att.jsonValue() )
        return L

def attrFromJsonValue(val):
    # keyword must be strings
    val1 = {}
    for (a,b) in val.items():
        val1[str(a)] = b
    return Attribute(**val1)

class Attribute:
    def __init__(self, name, typeName, isIdentifier= False, isMandatory= True, isOnto= False, isOneToOne= False,
                 isMultiple= False):
        self.name = name
        self.typeName = typeName
        self.isIdentifier = isIdentifier
        self.isMandatory = isMandatory
        self.isOnto = isOnto
        self.isOneToOne = isOneToOne
        self.isMultiple = isMultiple
    def flatten(self, setValue, setName, schema):
        name = self.name
        isMultiple = self.isMultiple
        isOneToOne = self.isOneToOne
        typeName = self.typeName
        isLink = schema.hasDescriptionSet(typeName)
        isIdentifier = self.isIdentifier
        if isMultiple:
            assert not isIdentifier, "identifier should not be multiple "+repr((setName, name))
            if isOneToOne and isLink:
                # reverse the reference
                reverseName = "%s_%s" % (setName, name)
                attr = Attribute(reverseName, setName, isIdentifier=False,
                                 isMandatory=self.isOnto,
                                 isOnto=self.isMandatory,
                                 isOneToOne=False) # multiple reversed==not 1-1
                schema.addAttribute(typeName, reverseName, attr)
            else:
                # expand into a relationship set
                relationshipName = "%s_%s" % (setName, name)
                linkToSelfName = "%s_%s" % (relationshipName, setName)
                linkToOtherName = "%s_%s" % (relationshipName, typeName)
                schema.addDescriptionSet(relationshipName)
                selfAttr = Attribute(linkToSelfName, setName, isIdentifier=True,
                                     isMandatory=True, isOnto=self.isMandatory, isOneToOne=False)
                schema.addAttribute(relationshipName, linkToSelfName, selfAttr)
                otherAttr = Attribute(linkToOtherName, typeName, isIdentifier=True,
                                      isMandatory=True, isOnto=self.isOnto, isOneToOne=isOneToOne)
                schema.addAttribute(relationshipName, linkToOtherName, otherAttr)
        else:
            # not multiple, copy self into new schema
            attr = Attribute(name, self.typeName, isIdentifier, self.isMandatory,
                             self.isOnto, isOneToOne, isMultiple)
            schema.addAttribute(setName, name, attr)
    def jsonValue(self):
        D = {}
        for attr in "name typeName isIdentifier isMandatory isOnto isOneToOne isMultiple".split():
            D[attr] = getattr(self, attr)
        return D

